package il.ac.tau.cg.ex5.engine;

import il.ac.tau.cg.ex5.config.SceneConstants;
import il.ac.tau.cg.ex5.lighting.DirectedLighter;
import il.ac.tau.cg.ex5.lighting.ILighter;
import il.ac.tau.cg.ex5.lighting.PointLighter;
import il.ac.tau.cg.ex5.lighting.SpotLighter;
import il.ac.tau.cg.ex5.obj.ObjEntity;
import il.ac.tau.cg.ex5.rendering.BruteForceRenderer;
import il.ac.tau.cg.ex5.rendering.DisplayListsRenderer;
import il.ac.tau.cg.ex5.rendering.IRenderer;
import il.ac.tau.cg.ex5.rendering.VertexArraysRenderer;
import il.ac.tau.cg.ex5.util.Color;
import il.ac.tau.cg.ex5.util.Face;
import il.ac.tau.cg.ex5.util.Vertex;

import java.awt.Component;
import java.awt.Frame;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.media.opengl.GL;
import javax.media.opengl.GL2;
import javax.media.opengl.GL2ES1;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.awt.GLCanvas;
import javax.media.opengl.fixedfunc.GLLightingFunc;
import javax.media.opengl.fixedfunc.GLMatrixFunc;
import javax.media.opengl.glu.GLU;

import com.jogamp.opengl.util.Animator;

public class Engine implements GLEventListener, KeyListener
{
	private GLU glu;
	private GLCanvas canvas;
	private Frame frame;
	private Animator animator;
	
	private Vertex translation;
	private float scale;
	private float rotation;
	
	private boolean rotationPaused;
	
	private IRenderer[] renderers;
	private int rendererIndex;
	
	private ILighter[] lighters;
	private int lighterIndex;
	
	public Engine(ObjEntity objEntity)
	{
		this.glu = new GLU();
		this.canvas = new GLCanvas();
		this.frame = new Frame("Computer Graphics Exercise 5");
		this.animator = new Animator(canvas);
		
		ObjEntity roomEntity = createRoomEntity();
		
		this.renderers = new IRenderer[]
		{
			new BruteForceRenderer(roomEntity, objEntity),
			new VertexArraysRenderer(roomEntity, objEntity),
			new DisplayListsRenderer(roomEntity, objEntity)
		};
		
		this.lighters = new ILighter[]
		{
			new PointLighter(),
			new DirectedLighter(),
			new SpotLighter()
		};
		
		this.rendererIndex = 0;
		this.lighterIndex = 0;
		
		this.translation = objEntity.calculateCenterMass().negate();
		this.scale = objEntity.calculateIdentityScale();
		this.rotation = SceneConstants.OBJ_INITIAL_ROTATION;
		
		this.rotationPaused = false;
	}
	
	public void start()
	{
		canvas.addGLEventListener(this);
		
		frame.add(canvas);
		frame.setSize(1024, 768);
		frame.setUndecorated(true);
		//frame.setExtendedState(Frame.MAXIMIZED_BOTH);
		
		frame.addWindowListener(new WindowAdapter()
		{
			public void windowClosing(WindowEvent e)
			{
				stop();
			}
		});
		
		frame.setVisible(true);
		
		animator.start();
		canvas.requestFocus();
	}
	
	public void stop()
	{
		animator.stop();
		frame.dispose();
		
		System.exit(0);
	}
	
	@Override
	public void init(GLAutoDrawable glDrawable)
	{
		GL2 gl = glDrawable.getGL().getGL2();
		
		gl.glShadeModel(GLLightingFunc.GL_SMOOTH);
		gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
		gl.glClearDepth(1.0f);
		gl.glEnable(GL.GL_DEPTH_TEST);
		gl.glDepthFunc(GL.GL_LEQUAL);
		gl.glHint(GL2ES1.GL_PERSPECTIVE_CORRECTION_HINT, GL.GL_NICEST);
		
		gl.glEnable(GLLightingFunc.GL_COLOR_MATERIAL);
		gl.glColorMaterial(GL.GL_FRONT_AND_BACK, GLLightingFunc.GL_AMBIENT_AND_DIFFUSE);
		
		gl.glEnable(GLLightingFunc.GL_LIGHTING);
		
		((Component)glDrawable).addKeyListener(this);
		
		for (ILighter lighter : lighters)
		{
			lighter.init(gl);
		}
		
		for (IRenderer renderer : renderers)
		{
			renderer.init(gl);
		}
	}
	
	@Override
	public void display(GLAutoDrawable glDrawable)
	{
		GL2 gl = glDrawable.getGL().getGL2();
		
		gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
		gl.glDisable(GLLightingFunc.GL_LIGHT0);
		gl.glDisable(GLLightingFunc.GL_LIGHT1);
		gl.glDisable(GLLightingFunc.GL_LIGHT2);
		
		gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
		gl.glLoadIdentity();
		gl.glTranslatef(0.0f, 0.0f, -4.0f);
		
		lighters[lighterIndex].set(gl);
		renderers[rendererIndex].render(gl, translation, scale, rotation);
		
		if (!isRotationPaused())
		{
			advanceRotation();
		}
	}
	
	@Override
	public void dispose(GLAutoDrawable glDrawable)
	{
		
	}

	@Override
	public void reshape(GLAutoDrawable glDrawable, int x, int y, int width, int height)
	{
		GL2 gl = glDrawable.getGL().getGL2();
		
		float aspectRatio = (float)width / (float)height;
		
		gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION);
		gl.glLoadIdentity();
		
		glu.gluPerspective(50.0f, aspectRatio, 1.0, 1000.0);
		
		gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
		gl.glLoadIdentity();
	}
	
	private void toggleRendering()
	{
		rendererIndex = ((rendererIndex + 1) % renderers.length);
		
		System.out.println("Current renderer: " + renderers[rendererIndex].getName() + ".");
	}
	
	private void toggleLighting()
	{
		lighterIndex = ((lighterIndex + 1) % lighters.length);
		
		System.out.println("Current lighter: " + lighters[lighterIndex].getName() + ".");
	}
	
	private void toggleRotation()
	{
		if (isRotationPaused())
		{
			resumeRotation();
			
			System.out.println("Rotation resumed.");
		}
		else
		{
			pauseRotation();
			
			System.out.println("Rotation paused.");
		}
	}
	
	@Override
	public void keyPressed(KeyEvent e)
	{
		switch (e.getKeyCode())
		{
			case KeyEvent.VK_ESCAPE:	stop();				return;
			case KeyEvent.VK_SPACE:		toggleRotation();	return;
			case KeyEvent.VK_L:			toggleLighting();	return;
			case KeyEvent.VK_R:			toggleRendering();	return;
		}
	}

	@Override
	public void keyReleased(KeyEvent e)
	{
		
	}

	@Override
	public void keyTyped(KeyEvent e)
	{
		
	}
	
	private void advanceRotation()
	{
		rotation += SceneConstants.OBJ_ROTATION_STEP;
	}
	
	private boolean isRotationPaused()
	{
		return rotationPaused;
	}
	
	private void pauseRotation()
	{
		rotationPaused = true;
	}
	
	private void resumeRotation()
	{
		rotationPaused = false;
	}
	
	private ObjEntity createRoomEntity()
	{
		Face[] faces = new Face[5]; //TODO: 6
		
		// Left wall
		Vertex[] vertices = new Vertex[4];
		Vertex[] normals = new Vertex[4];
		Color[] colors = new Color[4];
		
		vertices[0] = new Vertex(SceneConstants.ROOM_LEFT, SceneConstants.ROOM_TOP, SceneConstants.ROOM_FRONT);
		vertices[1] = new Vertex(SceneConstants.ROOM_LEFT, SceneConstants.ROOM_TOP, SceneConstants.ROOM_BACK);
		vertices[2] = new Vertex(SceneConstants.ROOM_LEFT, SceneConstants.ROOM_BOTTOM, SceneConstants.ROOM_BACK);
		vertices[3] = new Vertex(SceneConstants.ROOM_LEFT, SceneConstants.ROOM_BOTTOM, SceneConstants.ROOM_FRONT);
		normals[0] = new Vertex(1.0f, 0.0f, 0.0f);
		normals[1] = new Vertex(1.0f, 0.0f, 0.0f);
		normals[2] = new Vertex(1.0f, 0.0f, 0.0f);
		normals[3] = new Vertex(1.0f, 0.0f, 0.0f);
		colors[0] = new Color(0.8f, 0.0f, 0.0f);
		colors[1] = new Color(0.8f, 0.0f, 0.0f);
		colors[2] = new Color(0.8f, 0.0f, 0.0f);
		colors[3] = new Color(0.8f, 0.0f, 0.0f);
		
		faces[0] = new Face(vertices, normals, colors);

		// Right wall
		vertices = new Vertex[4];
		normals = new Vertex[4];
		colors = new Color[4];
		
		vertices[0] = new Vertex(SceneConstants.ROOM_RIGHT, SceneConstants.ROOM_TOP, SceneConstants.ROOM_BACK);
		vertices[1] = new Vertex(SceneConstants.ROOM_RIGHT, SceneConstants.ROOM_TOP, SceneConstants.ROOM_FRONT);
		vertices[2] = new Vertex(SceneConstants.ROOM_RIGHT, SceneConstants.ROOM_BOTTOM, SceneConstants.ROOM_FRONT);
		vertices[3] = new Vertex(SceneConstants.ROOM_RIGHT, SceneConstants.ROOM_BOTTOM, SceneConstants.ROOM_BACK);
		normals[0] = new Vertex(-1.0f, 0.0f, 0.0f);
		normals[1] = new Vertex(-1.0f, 0.0f, 0.0f);
		normals[2] = new Vertex(-1.0f, 0.0f, 0.0f);
		normals[3] = new Vertex(-1.0f, 0.0f, 0.0f);
		colors[0] = new Color(0.0f, 0.8f, 0.0f);
		colors[1] = new Color(0.0f, 0.8f, 0.0f);
		colors[2] = new Color(0.0f, 0.8f, 0.0f);
		colors[3] = new Color(0.0f, 0.8f, 0.0f);
		
		faces[1] = new Face(vertices, normals, colors);
		
		// Top, back, and bottom walls (in this order)
		vertices = new Vertex[4];
		normals = new Vertex[4];
		colors = new Color[4];
		
		vertices[0] = new Vertex(SceneConstants.ROOM_LEFT, SceneConstants.ROOM_TOP, SceneConstants.ROOM_FRONT);
		vertices[1] = new Vertex(SceneConstants.ROOM_RIGHT, SceneConstants.ROOM_TOP, SceneConstants.ROOM_FRONT);
		vertices[2] = new Vertex(SceneConstants.ROOM_RIGHT, SceneConstants.ROOM_TOP, SceneConstants.ROOM_BACK);
		vertices[3] = new Vertex(SceneConstants.ROOM_LEFT, SceneConstants.ROOM_TOP, SceneConstants.ROOM_BACK);
		normals[0] = new Vertex(0.0f, -1.0f, 0.0f);
		normals[1] = new Vertex(0.0f, -1.0f, 0.0f);
		normals[2] = new Vertex(0.0f, -1.0f, 0.0f);
		normals[3] = new Vertex(0.0f, -1.0f, 0.0f);
		colors[0] = new Color(0.90f, 0.75f, 0.50f);
		colors[1] = new Color(0.90f, 0.75f, 0.50f);
		colors[2] = new Color(0.90f, 0.75f, 0.50f);
		colors[3] = new Color(0.90f, 0.75f, 0.50f);
		
		faces[2] = new Face(vertices, normals, colors);

		vertices = new Vertex[4];
		normals = new Vertex[4];
		colors = new Color[4];
		
		vertices[0] = new Vertex(SceneConstants.ROOM_LEFT, SceneConstants.ROOM_TOP, SceneConstants.ROOM_BACK);
		vertices[1] = new Vertex(SceneConstants.ROOM_RIGHT, SceneConstants.ROOM_TOP, SceneConstants.ROOM_BACK);
		vertices[2] = new Vertex(SceneConstants.ROOM_RIGHT, SceneConstants.ROOM_BOTTOM, SceneConstants.ROOM_BACK);
		vertices[3] = new Vertex(SceneConstants.ROOM_LEFT, SceneConstants.ROOM_BOTTOM, SceneConstants.ROOM_BACK);
		normals[0] = new Vertex(0.0f, 0.0f, 1.0f);
		normals[1] = new Vertex(0.0f, 0.0f, 1.0f);
		normals[2] = new Vertex(0.0f, 0.0f, 1.0f);
		normals[3] = new Vertex(0.0f, 0.0f, 1.0f);
		colors[0] = new Color(0.90f, 0.75f, 0.50f);
		colors[1] = new Color(0.90f, 0.75f, 0.50f);
		colors[2] = new Color(0.90f, 0.75f, 0.50f);
		colors[3] = new Color(0.90f, 0.75f, 0.50f);
		
		faces[3] = new Face(vertices, normals, colors);

		vertices = new Vertex[4];
		normals = new Vertex[4];
		colors = new Color[4];
		
		vertices[0] = new Vertex(SceneConstants.ROOM_LEFT, SceneConstants.ROOM_BOTTOM, SceneConstants.ROOM_BACK);
		vertices[1] = new Vertex(SceneConstants.ROOM_RIGHT, SceneConstants.ROOM_BOTTOM, SceneConstants.ROOM_BACK);
		vertices[2] = new Vertex(SceneConstants.ROOM_RIGHT, SceneConstants.ROOM_BOTTOM, SceneConstants.ROOM_FRONT);
		vertices[3] = new Vertex(SceneConstants.ROOM_LEFT, SceneConstants.ROOM_BOTTOM, SceneConstants.ROOM_FRONT);
		normals[0] = new Vertex(0.0f, 1.0f, 0.0f);
		normals[1] = new Vertex(0.0f, 1.0f, 0.0f);
		normals[2] = new Vertex(0.0f, 1.0f, 0.0f);
		normals[3] = new Vertex(0.0f, 1.0f, 0.0f);
		colors[0] = new Color(0.90f, 0.75f, 0.50f);
		colors[1] = new Color(0.90f, 0.75f, 0.50f);
		colors[2] = new Color(0.90f, 0.75f, 0.50f);
		colors[3] = new Color(0.90f, 0.75f, 0.50f);

		faces[4] = new Face(vertices, normals, colors);
		
		//TODO: add front wall (behind the camera)

		return new ObjEntity(faces);
	}
}
